Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure reload sets correct owner for each association (ActiveRecord::Persistence) #46383

Conversation

dmytro-savochkin
Copy link
Contributor

@dmytro-savochkin dmytro-savochkin commented Oct 29, 2022

Motivation / Background

Fixes #46363 which was caused by #41112

Currently calling reload re-calculates the association cache based on the object freshly loaded from the database. The fresh object gets assigned as owner for each association in the association cache. That object however is not the same as self (even though they both point to the same record). Under some specific circumstances (using has_one/has_many through associations and transactions with after_commit callbacks) this might lead to losing the effects of assignments done inside the callback.

This commit fixes it by making reload point to self as owner for each association in the association cache.

I think this is safe because as far as I can see in https://github.com/rails/rails/blob/main/activerecord/lib/active_record/associations.rb#L320 it always passes self when creating a new association.

Detail

This Pull Request changes ActiveRecord::Persistence#reload method.

Checklist

Before submitting the PR make sure the following are checked:

  • This Pull Request is related to one change. Changes that are unrelated should be opened in separate PRs.
  • Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex: [Fix #issue-number]
  • Tests are added or updated if you fix a bug or add a feature.
  • CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included.
  • CI is passing.

@dmytro-savochkin
Copy link
Contributor Author

@eileencodes, may I ask you to review please (since you were adding association cache line before)? Thanks!

@@ -971,6 +971,7 @@ def reload(options = nil)
end

@association_cache = fresh_object.instance_variable_get(:@association_cache)
@association_cache.each_value { |association| association.instance_variable_set(:@owner, self) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what core team folks think on that but I always feel uncomfortable about using instance_variable_set unless we are patching some code we have no access to. Otherwise we are using something that wasn't supposed to be used even within the framework itself and makes me think whether we should add a public method to association to be able to set the owner. To be fair it might not be a solid argument as we already set and access instance variables on the lines above and below this one 🙃

Other than that, the PR makes sense to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was exactly my thinking!

I am also not very comfortable using instance_variable_get/instance_variable_set but in this particular case I decided to go with it because:

  1. This is already done one line above and below just as you said :)
  2. I wasn't completely sure my approach would be accepted (dunno, maybe some other reasons I am not aware of) so doing some major change, like trying to introduce some new public method into association (which in turn might require changes to the docs, etc, etc) so that it can be used in this place, seemed a bit too premature to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if you think it's worth it, let me know, and I will try to introduce my change via a new public method.
However the lines above and below will still be using instance_variable_get, so dunno... Changing those in this PR as well seems like a bit too much for such a small problem/PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say feel free to leave it as is for now, your reasoning makes total sense 👍 I guess I would have done the same to avoid prematurely overcomplicating the proposal 🙃

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let's avoid those instance variable set here. If we need to set the owner outside of that object, we should expose a writter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, updated.

@dmytro-savochkin
Copy link
Contributor Author

Hey @nvasilevski!
Sorry for bothering but maybe you know who could be added to reviewers to this?
I understand sometimes it might take a few months here for a PR to be addressed but until I am completely sure it's the only option I'd like to try fasten things up a little :)

@skipkayhil
Copy link
Member

Please don't ping individuals for review unless they have asked you to. The best way to get feedback would be asking in the Discord

@dmytro-savochkin
Copy link
Contributor Author

Please don't ping individuals for review unless they have asked you to. The best way to get feedback would be asking in the Discord

Oh, wow, there is also a Discord! Thanks, I will try that.
So far however I've followed this guide for feedback and on the only thing mentioned there (ror-core mailing list) there was no reply.

@dmytro-savochkin
Copy link
Contributor Author

This PR is still relevant for Rails 7.1.1, the issue is still there.

@zzak zzak added this to the 7.1.2 milestone Oct 20, 2023
Comment on lines 5 to 7
# The test case below is aimed to fix a bug that was noticeable only with a very specific
# setup of models. The models are fully defined inside the test case so that they are not
# accidentally changed later causing the bug effects to disappear.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this test to where it makes sense. You can define new models by creating new classes in the test/models folder, no need to do what is being done here.

Those models here still leaks to all the other files, so you aren't really isolating those models from other tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, updated as well.

@@ -971,6 +971,7 @@ def reload(options = nil)
end

@association_cache = fresh_object.instance_variable_get(:@association_cache)
@association_cache.each_value { |association| association.instance_variable_set(:@owner, self) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let's avoid those instance variable set here. If we need to set the owner outside of that object, we should expose a writter.

@rafaelfranca rafaelfranca modified the milestones: 7.1.2, 7.1.3 Nov 10, 2023
Fix rails#46363.

Currently calling `reload` re-calculates the association cache based
on the object freshly loaded from the database. The fresh object gets
assigned as `owner` for each association in the association cache.
That object however is not the same as `self` (even though they
both point to the same record). Under some specific circumstances
(using `has_one/has_many through` associations and transactions with
`after_commit` callbacks) this might lead to losing the effects of
assignments done inside the callback.

This commit fixes it by making `reload` point to `self` as `owner`
for each association in the association cache.
@dmytro-savochkin dmytro-savochkin force-pushed the fix-activerecord-reload-association-cache branch from 70ef26f to 781ecc2 Compare November 18, 2023 14:09
@rafaelfranca rafaelfranca merged commit af2bbd5 into rails:main Jan 15, 2024
4 checks passed
rafaelfranca added a commit that referenced this pull request Jan 15, 2024
…ad-association-cache

Ensure `reload` sets correct owner for each association (ActiveRecord::Persistence)
rafaelfranca added a commit that referenced this pull request Jan 15, 2024
@dmytro-savochkin dmytro-savochkin deleted the fix-activerecord-reload-association-cache branch January 18, 2024 14:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Loss of state after reload, transaction, and after_save_commit (regression in ActiveRecord 7.0)
5 participants